package jmine.tec.test.utils.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import junit.framework.Test;
import junit.framework.TestResult;
import junit.framework.TestSuite;

import org.apache.log4j.Logger;

/**
 * Suite that fires a test consuming thread for each available core of the machine.
 * 
 * @author jrenaut
 */
public class MultiThreadTestSuite extends TestSuite {

    private static final int DEFAULT_THREADS_PER_CORE = 1;

    private LinkedBlockingQueue<TestInstance> queue = new LinkedBlockingQueue<TestInstance>();

    private CountDownLatch startLatch = new CountDownLatch(1);

    private CountDownLatch finishLatch;

    private List<TestRunner> runners;

    private Logger logger = Logger.getLogger(MultiThreadTestSuite.class);

    private int threadsPerCore = DEFAULT_THREADS_PER_CORE;

    private long t0;

    private long t1;

    /**
     * C'tor
     */
    public MultiThreadTestSuite() {
        super();
    }

    /**
     * C'tor
     * 
     * @param testName String
     */
    public MultiThreadTestSuite(final String testName) {
        super(testName);
    }

    /**
     * Sets the thread creation multipler. The amount of available processors to the runtime * this multiplier threads will be created.
     * 
     * @param tpc int
     */
    public void setThreadsPerCore(final int tpc) {
        if (tpc < 1) {
            throw new IllegalArgumentException("Threads per core should be greater than 0.");
        }
        this.threadsPerCore = tpc;
    }

    /**
     * Runs all tests of the suite.
     * 
     * @param result TestResult
     */
    @Override
    public void run(final TestResult result) {
        this.beforeTests();
        // this.finishLatch = new CountDownLatch(this.testCount(this));
        this.finishLatch = new CountDownLatch(this.testCount());

        super.run(result);
        this.startLatch.countDown();

        try {
            this.finishLatch.await();
        } catch (InterruptedException e) {
            this.warn("Interrupted while waiting for finish latch!");
        }

        this.afterTests();
    }

    // /**
    // * Returns the number of tests contained by this test.
    // *
    // * If this is not a test suite returns 1.
    // *
    // * @param test
    // * Test
    // * @return the count
    // */
    // private int testCount(final Test test) {
    // if (test instanceof TestSuite) {
    // TestSuite ts = (TestSuite) test;
    // int total = 0;
    // for (Enumeration e = ts.tests(); e.hasMoreElements();) {
    // Test t = (Test) e.nextElement();
    // total += this.testCount(t);
    // }
    // return total;
    // }
    // return 1;
    // }

    /**
     * Starts a consumer thread per processor.
     */
    protected void beforeTests() {

        this.t0 = System.currentTimeMillis();

        int availableProcessors = Runtime.getRuntime().availableProcessors() * this.threadsPerCore;

        this.runners = new ArrayList<TestRunner>(availableProcessors);

        for (int i = 0; i < availableProcessors; i++) {
            TestRunner runner = new TestRunner();
            this.runners.add(runner);
            runner.start();
        }
    }

    /**
     */
    protected void afterTests() {
        for (TestRunner t : this.runners) {
            t.interrupt();
            this.info("Thread [" + t.hashCode() + "] ran " + t.getCount());
            try {
                t.join();
            } catch (InterruptedException e) {
                this.warn("Interrupted while joining " + t);
            }
        }

        this.t1 = System.currentTimeMillis();
        this.info("Suite took " + (this.t1 - this.t0) + " millis.");
    }

    /**
     * Executa o teste.
     * 
     * @param test Test
     * @param result TestResult
     */
    @Override
    public void runTest(final Test test, final TestResult result) {
        // if (test instanceof TestSuite) {
        // TestSuite ts = (TestSuite) test;
        // for (Enumeration e = ts.tests(); e.hasMoreElements();) {
        // Test t = (Test) e.nextElement();
        // TestInstance ti = new TestInstance(t, result);
        // this.queue.add(ti);
        // }
        // } else {
        TestInstance ti = new TestInstance(test, result);
        this.queue.add(ti);
        // }
    }

    /**
     * Holder para uma instância de teste que precisa ser executada
     * 
     * @author jrenaut
     */
    private static class TestInstance {
        private Test test;

        private TestResult result;

        /**
         * C'tor
         * 
         * @param test Test
         * @param result TestResult
         */
        public TestInstance(final Test test, final TestResult result) {
            super();
            this.test = test;
            this.result = result;
        }

        /**
         * Executa este teste.
         */
        public void executa() {
            this.test.run(this.result);
        }
    }

    /**
     * Executa testes de uma fila.
     * 
     * @author jrenaut
     */
    private class TestRunner extends Thread {
        private static final int WAIT_TIME = 10;

        private boolean interrupted = false;

        private int count = 0;

        /**
         * Executa
         */
        @Override
        public void run() {
            MultiThreadTestSuite.this.info("[" + this + "] Waiting...");
            try {
                MultiThreadTestSuite.this.startLatch.await();
            } catch (InterruptedException e) {
                MultiThreadTestSuite.this.warn("[" + this + "] Interrupted while waiting!");
                return;
            }

            MultiThreadTestSuite.this.info("[" + this + "] Running...");
            while (!this.interrupted) {
                try {
                    TestInstance ti = MultiThreadTestSuite.this.queue.poll(WAIT_TIME, TimeUnit.MILLISECONDS);
                    if (ti != null) {
                        MultiThreadTestSuite.this.info("Running test on thread: " + this);
                        ti.executa();
                        this.count++;
                        MultiThreadTestSuite.this.finishLatch.countDown();
                    }
                } catch (InterruptedException e) {
                    MultiThreadTestSuite.this.info("[" + this + "] Interrupted while polling!");
                    return;
                }
            }
            MultiThreadTestSuite.this.info("[" + this + "] Stopping...");
        }

        /**
         * Returns how many tests were already run by this runner.
         * 
         * @return int
         */
        public int getCount() {
            return this.count;
        }

        /**
         * Stops test polling
         */
        @Override
        public void interrupt() {
            super.interrupt();
            this.interrupted = true;
        }

        /**
         * tostring
         * 
         * @return String
         */
        @Override
        public String toString() {
            return String.valueOf(this.hashCode());
        }
    }

    /**
     * Info
     * 
     * @param s String
     */
    protected void info(final String s) {
        this.logger.info(s);
    }

    /**
     * Warn
     * 
     * @param s String s
     */
    protected void warn(final String s) {
        this.logger.warn(s);
    }

}
